package com.fzk.blog.utils;

import java.util.*;
import java.util.stream.Collectors;

/**
 * 用户登陆的密码相似度评估
 */
public class MyPasswordSimilarUtil {
    //相似度阈值，相似度大于该阈值，认定没有风险，反之亦然
    private final Double thresholdSimilarity;

    //构造
    public MyPasswordSimilarUtil(Double thresholdSimilarity) {
        this.thresholdSimilarity = thresholdSimilarity;
    }


    /**
     * 逻辑执行，
     *
     * @param loginPassword   登录的密码
     * @param historyPassword 历史使用过的密码
     * @return 返回true, 代表密码相似度达到阈值，返回false，要么是历史密码为null或相似度和小
     */
    public Boolean isSimilar(String loginPassword, Set<String> historyPassword) {
        //如果用户是第一次登陆
        if (historyPassword == null || historyPassword.size() == 0) {
            return false;
        }
        //获取字符袋
        Set<Character> wordBag = new HashSet<>();
        historyPassword.stream()
                .forEach(c -> {
                    char[] chars = c.toCharArray();
                    for (char aChar : chars) {
                        wordBag.add(aChar);//加入到词袋中
                    }
                });
        //富华维度，可以把当前输入的字符页添加到字符袋中
        char[] chars = loginPassword.toCharArray();
        for (Character aChar : chars) {
            wordBag.add(aChar);
        }
        //把字符袋中的字符排序
        List<Character> wordList = wordBag.stream()
                .sorted()
                .collect(Collectors.toList());
        System.out.println("字符袋：" + wordList);
        //把历史密码转化为向量
        List<Integer[]> historyVector = historyPassword.stream()
                .map(p -> transformStringToVector(wordList, p))
                .collect(Collectors.toList());
        //把当前密码转化为向量
        Integer[] loginVector = transformStringToVector(wordList, loginPassword);
        //获取没有风险的相似度集合
        List<Double> similarities = historyVector.stream()
                .map(h -> {
                    Double similarity = calculateSimilarity(h, loginVector);
                    System.out.println("相似度：" + similarity);
                    return similarity;
                })
                .filter(s -> s > thresholdSimilarity)//过滤出有风险的相似度
                .collect(Collectors.toList());
        return similarities.size() > 0;
    }

    /**
     * 把密码乱序转化为向量
     *
     * @param wordBag  所有历史密码组成的排序好的字符袋
     * @param password 要转化的密码乱序
     * @return 转化后的向量
     */
    private Integer[] transformStringToVector(List<Character> wordBag, String password) {
        //转化后的向量
        Integer[] vector = new Integer[wordBag.size()];
        //把密码乱序转化为map集合(字符，个数)
        Map<Character, Integer> passCharMap = new HashMap<>();
        char[] chars = password.toCharArray();
        //遍历字符数组
        for (Character c : chars) {
            //初始数量
            Integer count = 1;
            //如果map中有该字符
            if (passCharMap.containsKey(c)) {
                count += passCharMap.get(c);
            }
            //放入到map中
            passCharMap.put(c, count);
        }
        //遍历字符袋,如果密码map中有该字符，向量该字符特征的属性就+1，
        for (int i = 0; i < wordBag.size(); i++) {
            Character c = wordBag.get(i);
            vector[i] = passCharMap.containsKey(c) ? passCharMap.get(c) : 0;
        }
        return vector;
    }

    /**
     * 计算两个向量的相似度
     *
     * @param v1 v1向量
     * @param v2 v2向量
     * @return 返回相似度
     */
    private Double calculateSimilarity(Integer[] v1, Integer[] v2) {
        //计算两个向量的夹角余弦,向量点乘/模的乘积
        //向量的点乘：v1 * v2
        double dotMulti = 0.0d;
        for (int i = 0; i < v1.length; i++) {
            dotMulti += v1[i] * v2[i];
        }
        //计算两个向量的模的平方
        Integer length1 = Arrays.stream(v1)
                .map(v -> v * v)
                .reduce((i1, i2) -> i1 + i2)
                .get();
        Integer length2 = Arrays.stream(v2)
                .map(v -> v * v)
                .reduce((i1, i2) -> i1 + i2)
                .get();
        //计算余弦
        return dotMulti / (Math.sqrt(length1) * Math.sqrt(length2));
    }
}

